Skip to content

vue 面试题

1. vue2有哪些生命周期?和vue3有什么区别?

Details

在 Vue.js 中,生命周期是指 Vue 实例从创建到销毁的各个阶段。在 Vue 2 和 Vue 3 中,生命周期钩子存在一些相似之处,但在 Vue 3 中引入了组合式 API,使得生命周期的使用方式有所不同。以下是 Vue 2 和 Vue 3 的生命周期钩子的详细说明及其区别。

Vue 2 的生命周期钩子

Vue 2 中的生命周期钩子主要包括以下几个:

  1. beforeCreate:实例初始化之后,数据观测和事件配置之前调用。
  2. created:实例创建完成后调用,此时数据已被观测,事件已被设置,但尚未挂载。
  3. beforeMount:在挂载开始之前调用,此时模板已编译,但未被插入 DOM。
  4. mounted:挂载完成后调用,此时 DOM 已经被插入,适合进行 DOM 操作。
  5. beforeUpdate:数据更新前调用,可以在这里访问更新前的状态。
  6. updated:数据更新后调用,此时 DOM 已经重新渲染。
  7. beforeDestroy:实例销毁前调用,可以在这里做一些清理工作,如取消订阅。
  8. destroyed:实例销毁后调用,此时所有的事件监听和子实例都已被移除。

Vue 3 的生命周期钩子

Vue 3 的生命周期钩子与 Vue 2 类似,但可以通过组合式 API 来调用。以下是 Vue 3 的生命周期钩子(与 Vue 2 相同):

  1. beforeCreate:同 Vue 2。
  2. created:同 Vue 2。
  3. beforeMount:同 Vue 2。
  4. mounted:同 Vue 2。
  5. beforeUpdate:同 Vue 2。
  6. updated:同 Vue 2。
  7. beforeUnmount:取代 Vue 2 中的 beforeDestroy,在实例销毁之前调用。
  8. unmounted:取代 Vue 2 中的 destroyed,在实例销毁后调用。

Vue 2 和 Vue 3 的主要区别

  1. 名称变化:Vue 3 中将 beforeDestroy 改名为 beforeUnmount,将 destroyed 改名为 unmounted,使名称更符合直观操作。

  2. 组合式 API:在 Vue 3 中,生命周期钩子可以在 setup() 函数内使用,您可以通过导入相应的生命周期钩子进行调用。例如:

    javascript
    import { onMounted, onBeforeUnmount } from 'vue';
    
    export default {
      setup() {
        onMounted(() => {
          console.log('组件已挂载');
        });
    
        onBeforeUnmount(() => {
          console.log('组件即将卸载');
        });
      }
    }
  3. 更好的逻辑复用:通过组合式 API,生命周期钩子可以更灵活地被组织和复用,而不必局限于选项式 API 中的 datamethods 等。

总结

Vue 2 和 Vue 3 的生命周期钩子在功能上保持一致,但 Vue 3 引入的组合式 API 使得生命周期的使用更灵活、逻辑复用更简单。

2. Vue 的双向数据绑定原理

Details

Vue 的双向数据绑定是其核心特性之一,它使得数据模型与视图之间能够自动同步,无需手动操作 DOM。Vue 的双向数据绑定原理主要基于以下两个机制:

1. 数据劫持

Vue 2 使用 Object.defineProperty() 方法对数据对象的属性进行劫持,当属性被访问或修改时,会触发相应的 getter 和 setter 方法。

javascript
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('get:', key);
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      console.log('set:', key, newVal);
      val = newVal;
      // 通知更新
    }
  });
}

Vue 3 使用 Proxy 对象对数据对象进行代理,相比 Object.defineProperty()Proxy 可以监听对象的所有属性,包括新增属性和删除属性。

javascript
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('get:', key);
      return target[key];
    },
    set(target, key, value) {
      console.log('set:', key, value);
      target[key] = value;
      return true;
    }
  });
}

2. 发布-订阅模式

Vue 使用发布-订阅模式来实现数据变化时的视图更新。当数据发生变化时,会通知所有订阅该数据的组件进行更新。

  1. Observer:观察者,负责监听数据变化并通知订阅者。
  2. Watcher:订阅者,负责接收数据变化的通知并更新视图。
  3. Dep:依赖收集器,负责管理订阅者列表。

双向绑定的实现

Vue 的双向数据绑定通过 v-model 指令实现,它是 v-bindv-on 的语法糖:

html
<input v-model="message">

等价于:

html
<input :value="message" @input="message = $event.target.value">

总结

Vue 的双向数据绑定原理基于数据劫持和发布-订阅模式,通过 Object.defineProperty()(Vue 2)或 Proxy(Vue 3)对数据进行监听,当数据变化时,通知订阅者更新视图,从而实现数据模型与视图的自动同步。

3. Vue 的 computed 和 watch 的区别

Details

computedwatch 都是 Vue 中用于响应式数据处理的重要特性,但它们的使用场景和实现原理有所不同。

computed

定义computed 是计算属性,用于根据依赖数据计算出一个新值。

特点

  1. 缓存性:计算属性会缓存计算结果,只有当依赖的数据发生变化时,才会重新计算。
  2. 响应式:计算属性是响应式的,当依赖的数据变化时,计算属性会自动更新。
  3. 声明式:计算属性是声明式的,更适合用于模板中显示计算结果。

使用场景

  • 当需要根据多个数据计算出一个新值时
  • 当需要缓存计算结果以提高性能时
  • 当计算逻辑比较复杂时

示例

javascript
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}

watch

定义watch 是监听器,用于监听数据变化并执行相应的操作。

特点

  1. 响应式:监听器会监听数据变化并执行回调函数。
  2. 副作用:监听器适合执行副作用操作,如 API 请求、DOM 操作等。
  3. 配置选项:可以配置 deep(深度监听)和 immediate(立即执行)等选项。

使用场景

  • 当需要在数据变化时执行异步操作或复杂逻辑时
  • 当需要监听对象的深层属性变化时
  • 当需要在组件初始化时立即执行监听逻辑时

示例

javascript
watch: {
  message: {
    handler(newVal, oldVal) {
      console.log('message changed:', newVal, oldVal);
    },
    deep: true,
    immediate: true
  }
}

主要区别

特性computedwatch
缓存
同步同步计算可异步
用途计算值执行副作用
依赖自动追踪依赖需要手动指定依赖
返回值必须返回值无返回值

总结

computed 适合用于计算衍生值,具有缓存性,更高效;watch 适合用于执行副作用操作,如 API 请求、DOM 操作等。在实际开发中,应根据具体场景选择合适的方式。

4. Vue 的 v-if 和 v-show 的区别

Details

v-ifv-show 都是 Vue 中用于条件渲染的指令,但它们的实现原理和使用场景有所不同。

v-if

定义v-if 是条件渲染指令,根据表达式的值决定是否渲染元素。

特点

  1. 惰性:当条件为 false 时,元素不会被渲染到 DOM 中。
  2. 条件切换:当条件从 false 变为 true 时,会创建元素;当条件从 true 变为 false 时,会销毁元素。
  3. 性能:切换成本较高,适合不频繁切换的场景。
  4. 作用域:可以与 v-elsev-else-if 配合使用,形成条件分支。

使用场景

  • 当条件不经常变化时
  • 当条件为 false 时,不需要元素存在于 DOM 中时
  • 当需要与 v-elsev-else-if 配合使用时

示例

html
<div v-if="isVisible">
  这是 v-if 渲染的内容
</div>
<div v-else>
  这是 v-else 渲染的内容
</div>

v-show

定义v-show 是条件显示指令,根据表达式的值决定是否显示元素。

特点

  1. 非惰性:无论条件是否为 true,元素都会被渲染到 DOM 中。
  2. 条件切换:当条件变化时,通过修改元素的 display CSS 属性来控制显示或隐藏。
  3. 性能:切换成本较低,适合频繁切换的场景。
  4. 作用域:不能与 v-elsev-else-if 配合使用。

使用场景

  • 当条件频繁变化时
  • 当条件为 false 时,元素可以存在于 DOM 中时
  • 当需要快速切换元素的显示状态时

示例

html
<div v-show="isVisible">
  这是 v-show 渲染的内容
</div>

主要区别

特性v-ifv-show
渲染方式条件渲染,不满足条件时不渲染始终渲染,通过 CSS 控制显示
切换成本较高,需要创建/销毁元素较低,只修改 CSS 属性
初始渲染满足条件时才渲染始终渲染
适用场景不频繁切换的场景频繁切换的场景
与 v-else 配合可以不可以

总结

v-if 适合用于不频繁切换的场景,因为它的切换成本较高,但初始渲染成本较低;v-show 适合用于频繁切换的场景,因为它的切换成本较低,但初始渲染成本较高。在实际开发中,应根据具体场景选择合适的指令。

5. Vue 的路由原理

Details

Vue Router 是 Vue 官方的路由管理器,它允许我们在单页应用中实现页面导航。Vue Router 的核心原理包括以下几个方面:

1. 路由模式

Vue Router 支持两种路由模式:

hash 模式

  • 使用 URL 的 hash 部分(# 后面的部分)来模拟路由
  • 优点:兼容性好,不需要服务器配置
  • 缺点:URL 中会包含 # 符号

history 模式

  • 使用 HTML5 History API 来实现路由
  • 优点:URL 更美观,没有 # 符号
  • 缺点:需要服务器配置,否则会出现 404 错误

2. 路由匹配

Vue Router 通过路由配置来匹配 URL 和组件:

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Home
    },
    {
      path: '/about',
      component: About
    }
  ]
});

当用户访问某个 URL 时,Vue Router 会根据路由配置找到对应的组件并渲染。

3. 路由导航

Vue Router 提供了多种导航方式:

声明式导航

html
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>

编程式导航

javascript
// 跳转到指定路径
this.$router.push('/about');

// 替换当前路径
this.$router.replace('/about');

// 后退
this.$router.back();

// 前进
this.$router.forward();

// 跳转到指定历史记录
this.$router.go(-1);

4. 路由守卫

Vue Router 提供了路由守卫来控制路由的访问权限:

全局守卫

javascript
// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 检查用户是否登录
  if (to.meta.requiresAuth && !isLoggedIn) {
    next('/login');
  } else {
    next();
  }
});

// 全局后置守卫
router.afterEach((to, from) => {
  // 记录页面访问日志
  console.log(`访问了 ${to.path}`);
});

路由独享守卫

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/admin',
      component: Admin,
      beforeEnter: (to, from, next) => {
        // 检查用户是否是管理员
        if (!isAdmin) {
          next('/');
        } else {
          next();
        }
      }
    }
  ]
});

组件内守卫

javascript
export default {
  beforeRouteEnter(to, from, next) {
    // 在组件渲染前调用
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由更新时调用
  },
  beforeRouteLeave(to, from, next) {
    // 在离开当前路由时调用
  }
};

5. 路由懒加载

为了提高应用的加载速度,Vue Router 支持路由懒加载:

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: () => import('./views/Home.vue')
    },
    {
      path: '/about',
      component: () => import('./views/About.vue')
    }
  ]
});

路由懒加载会将每个路由对应的组件打包成单独的文件,只有当用户访问该路由时才会加载对应的文件,从而减少初始加载时间。

总结

Vue Router 的核心原理包括路由模式、路由匹配、路由导航、路由守卫和路由懒加载等。通过这些机制,Vue Router 实现了单页应用的页面导航功能,为用户提供了流畅的浏览体验。

6. Vuex 的原理和使用

Details

Vuex 是 Vue 官方的状态管理库,用于管理 Vue 应用中的共享状态。Vuex 的核心原理和使用方法如下:

1. 核心概念

Vuex 包含以下核心概念:

State

  • 存储应用的状态
  • 是单一数据源

Getter

  • 从 State 中派生状态
  • 类似于计算属性

Mutation

  • 修改 State 的唯一方式
  • 必须是同步函数

Action

  • 处理异步操作
  • 可以提交 Mutation

Module

  • 将 Store 分割成模块
  • 每个模块有自己的 State、Getter、Mutation 和 Action

2. 基本使用

创建 Store

javascript
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    doubleCount: state => state.count * 2
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  }
});

export default store;

在组件中使用

javascript
// 访问 State
this.$store.state.count;

// 访问 Getter
this.$store.getters.doubleCount;

// 提交 Mutation
this.$store.commit('increment');

// 分发 Action
this.$store.dispatch('incrementAsync');

使用辅助函数

javascript
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount'])
  },
  methods: {
    ...mapMutations(['increment']),
    ...mapActions(['incrementAsync'])
  }
};

3. 工作原理

Vuex 的工作原理基于以下几点:

  1. 响应式:Vuex 的 State 是响应式的,当 State 发生变化时,所有依赖 State 的组件都会自动更新。

  2. 单向数据流

    • 组件通过 dispatch 触发 Action
    • Action 通过 commit 触发 Mutation
    • Mutation 修改 State
    • State 的变化触发组件更新
  3. 严格模式:在严格模式下,任何直接修改 State 的行为都会抛出错误,确保所有 State 的修改都通过 Mutation 进行。

4. 模块系统

当应用变得复杂时,可以使用模块系统将 Store 分割成多个模块:

javascript
const userModule = {
  namespaced: true,
  state: {
    user: null
  },
  getters: {
    getUser: state => state.user
  },
  mutations: {
    setUser(state, user) {
      state.user = user;
    }
  },
  actions: {
    fetchUser({ commit }) {
      // 异步获取用户信息
      commit('setUser', { id: 1, name: 'John' });
    }
  }
};

const store = new Vuex.Store({
  modules: {
    user: userModule
  }
});

在组件中使用模块

javascript
// 访问模块 State
this.$store.state.user.user;

// 访问模块 Getter
this.$store.getters['user/getUser'];

// 提交模块 Mutation
this.$store.commit('user/setUser', { id: 1, name: 'John' });

// 分发模块 Action
this.$store.dispatch('user/fetchUser');

// 使用辅助函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState('user', ['user']),
    ...mapGetters('user', ['getUser'])
  },
  methods: {
    ...mapMutations('user', ['setUser']),
    ...mapActions('user', ['fetchUser'])
  }
};

总结

Vuex 是一个专为 Vue 应用设计的状态管理库,它通过 State、Getter、Mutation、Action 和 Module 等核心概念,实现了应用状态的集中管理和单向数据流。在大型应用中,Vuex 可以帮助我们更好地组织和管理状态,提高代码的可维护性。

7. Vue 的 mixin 和 composition API 的区别

Details

Vue 2 中的 mixin 和 Vue 3 中的 composition API 都是用于代码复用的重要特性,但它们的实现方式和使用场景有所不同。

mixin

定义:mixin 是 Vue 2 中用于代码复用的一种方式,它允许我们将组件的选项(如 data、methods、computed 等)提取到一个单独的对象中,然后在多个组件中复用。

使用方式

javascript
// 定义 mixin
const myMixin = {
  data() {
    return {
      message: 'Hello from mixin'
    };
  },
  methods: {
    greet() {
      console.log(this.message);
    }
  },
  mounted() {
    this.greet();
  }
};

// 在组件中使用 mixin
export default {
  mixins: [myMixin],
  data() {
    return {
      message: 'Hello from component'
    };
  }
};

优点

  • 简单易用,适合小型项目
  • 可以在多个组件中复用代码

缺点

  • 命名冲突:当组件和 mixin 有相同名称的选项时,组件的选项会覆盖 mixin 的选项(数据对象除外,数据对象会合并)
  • 逻辑分散:mixin 中的逻辑分散在不同的选项中,难以追踪和维护
  • 隐式依赖:mixin 和组件之间的依赖关系不明确,容易导致代码混乱

composition API

定义:composition API 是 Vue 3 中引入的一种新的代码组织方式,它允许我们将相关的逻辑组织在一起,而不是按照选项类型分散在不同的选项中。

使用方式

javascript
// 定义 composable
import { ref, onMounted } from 'vue';

export function useGreeting() {
  const message = ref('Hello from composable');
  
  function greet() {
    console.log(message.value);
  }
  
  onMounted(() => {
    greet();
  });
  
  return {
    message,
    greet
  };
}

// 在组件中使用 composable
import { useGreeting } from './useGreeting';

export default {
  setup() {
    const { message, greet } = useGreeting();
    
    return {
      message,
      greet
    };
  }
};

优点

  • 逻辑组织:相关的逻辑可以组织在一起,提高代码的可读性和可维护性
  • 命名空间:通过返回值明确暴露给组件的状态和方法,避免命名冲突
  • 类型支持:更好的 TypeScript 支持
  • 逻辑复用:可以更灵活地复用逻辑,甚至在不同的组件之间共享逻辑

缺点

  • 学习成本:需要学习新的 API 和编程范式
  • 代码量:对于简单的逻辑,使用 composition API 可能会增加代码量

主要区别

特性mixincomposition API
代码组织按选项类型组织按逻辑功能组织
命名冲突可能发生命名冲突通过返回值避免命名冲突
依赖关系隐式依赖显式依赖
类型支持较差较好
逻辑复用有限灵活
可读性较差较好
维护性较差较好

总结

mixin 是 Vue 2 中用于代码复用的传统方式,适合小型项目;composition API 是 Vue 3 中引入的新方式,适合大型项目,提供了更好的代码组织和逻辑复用能力。在实际开发中,应根据项目的规模和复杂度选择合适的方式。

8. Vue 的性能优化技巧

Details

Vue 的性能优化是开发中非常重要的一环,以下是一些常见的 Vue 性能优化技巧:

1. 组件懒加载

路由懒加载

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: () => import('./views/Home.vue')
    },
    {
      path: '/about',
      component: () => import('./views/About.vue')
    }
  ]
});

组件懒加载

javascript
export default {
  components: {
    LazyComponent: () => import('./LazyComponent.vue')
  }
};

2. 虚拟滚动

当需要渲染大量数据时,使用虚拟滚动可以显著提高性能:

html
<template>
  <virtual-list
    :data-key="'id'"
    :data-sources="items"
    :data-component="ItemComponent"
    :estimate-size="54"
  />
</template>

3. 合理使用 v-if 和 v-show

  • v-if:适合不频繁切换的场景
  • v-show:适合频繁切换的场景

4. 使用 computed 缓存计算结果

javascript
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}

5. 使用 watch 的 deep 和 immediate 选项

javascript
watch: {
  user: {
    handler(newUser) {
      // 处理用户变化
    },
    deep: true, // 深度监听
    immediate: true // 立即执行
  }
}

6. 避免在模板中进行复杂计算

html
<!-- 不好的做法 -->
<div>{{ expensiveComputation() }}</div>

<!-- 好的做法 -->
<div>{{ computedValue }}</div>

7. 使用 Object.freeze() 冻结数据

对于不需要响应式的数据,可以使用 Object.freeze() 来冻结,减少 Vue 的响应式开销:

javascript
data() {
  return {
    staticData: Object.freeze([
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' }
    ])
  };
}

8. 合理使用 key

在使用 v-for 时,添加唯一的 key 可以帮助 Vue 更高效地更新 DOM:

html
<div v-for="item in items" :key="item.id">
  {{ item.name }}
</div>

9. 减少 watch 的使用

过多的 watch 会增加性能开销,尽量使用 computed 替代 watch。

10. 使用 keep-alive 缓存组件

对于频繁切换的组件,使用 keep-alive 可以缓存组件状态,避免重复渲染:

html
<keep-alive>
  <component :is="currentComponent"></component>
</keep-alive>

11. 优化图片加载

  • 使用适当的图片格式
  • 压缩图片
  • 使用懒加载

12. 减少 DOM 元素数量

避免不必要的 DOM 元素,减少 DOM 树的深度。

13. 使用生产环境构建

在生产环境中,Vue 会自动进行一些优化,如删除调试信息、压缩代码等。

14. 使用 Webpack 或 Vite 的优化

  • 代码分割
  • 树摇
  • 懒加载

总结

Vue 的性能优化需要从多个方面入手,包括组件懒加载、虚拟滚动、合理使用 v-if 和 v-show、使用 computed 缓存计算结果等。在实际开发中,应根据具体场景选择合适的优化策略,以提高应用的性能和用户体验。

9. Vue 3 的 composition API

Details

Vue 3 的 composition API 是 Vue 3 中引入的一种新的代码组织方式,它允许我们将相关的逻辑组织在一起,而不是按照选项类型分散在不同的选项中。

核心概念

setup() 函数

  • 是 composition API 的入口点
  • 在组件创建之前执行
  • 接收 props 和 context 作为参数
  • 返回值会暴露给模板和其他选项

响应式 API

  • ref():创建一个响应式的 ref 对象
  • reactive():创建一个响应式的对象
  • computed():创建一个计算属性
  • watch():创建一个监听器
  • watchEffect():创建一个自动追踪依赖的监听器

生命周期钩子

  • onMounted():组件挂载后调用
  • onUpdated():组件更新后调用
  • onUnmounted():组件卸载后调用
  • onBeforeMount():组件挂载前调用
  • onBeforeUpdate():组件更新前调用
  • onBeforeUnmount():组件卸载前调用
  • onErrorCaptured():捕获子组件错误时调用
  • onRenderTracked():响应式数据被追踪时调用
  • onRenderTriggered():响应式数据变化时调用

基本使用

javascript
import { ref, computed, onMounted } from 'vue';

export default {
  setup() {
    // 创建响应式数据
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);
    
    // 定义方法
    function increment() {
      count.value++;
    }
    
    // 使用生命周期钩子
    onMounted(() => {
      console.log('Component mounted');
    });
    
    // 返回值暴露给模板
    return {
      count,
      doubleCount,
      increment
    };
  }
};

优势

  1. 逻辑组织:相关的逻辑可以组织在一起,提高代码的可读性和可维护性
  2. 逻辑复用:可以更灵活地复用逻辑,甚至在不同的组件之间共享逻辑
  3. 类型支持:更好的 TypeScript 支持
  4. 树摇:未使用的代码可以被树摇掉,减少打包体积
  5. 更好的代码提示:IDE 可以提供更好的代码提示

与选项式 API 的对比

特性选项式 APIcomposition API
代码组织按选项类型组织按逻辑功能组织
逻辑复用通过 mixin通过 composable
类型支持较差较好
树摇较差较好
代码提示一般较好
学习成本

最佳实践

  1. 按功能组织逻辑:将相关的逻辑组织在一个 composable 中
  2. 使用 TypeScript:充分利用 TypeScript 的类型系统
  3. 保持 setup() 函数简洁:将复杂逻辑提取到 composable 中
  4. 使用 ref 和 reactive 合理:基本类型使用 ref,对象类型使用 reactive
  5. 避免在 setup() 中使用 this:setup() 函数中没有 this

总结

Vue 3 的 composition API 是一种新的代码组织方式,它提供了更好的逻辑组织和复用能力,以及更好的 TypeScript 支持。在大型项目中,composition API 可以显著提高代码的可维护性和可读性。

10. vue2和vue3的区别

Details

Vue 3 是 Vue.js 的重大版本更新,带来了许多新特性和改进。以下是 Vue 2 和 Vue 3 的主要区别:

1. 响应式系统

Vue 2

  • 使用 Object.defineProperty() 实现响应式
  • 无法监听对象的新增属性和删除属性
  • 无法监听数组的索引和长度变化

Vue 3

  • 使用 Proxy 实现响应式
  • 可以监听对象的新增属性和删除属性
  • 可以监听数组的索引和长度变化
  • 性能更好,特别是在大型应用中

2. 组合式 API

Vue 2

  • 使用选项式 API,按选项类型组织代码
  • 通过 mixin 实现代码复用
  • 逻辑分散,难以维护

Vue 3

  • 引入组合式 API,按逻辑功能组织代码
  • 通过 composable 实现代码复用
  • 逻辑集中,易于维护
  • 更好的 TypeScript 支持

3. 生命周期钩子

Vue 2

  • beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed

Vue 3

  • 选项式 API 中的生命周期钩子名称基本不变
  • 组合式 API 中使用函数形式的生命周期钩子,如 onMountedonUpdatedonUnmounted
  • beforeDestroy 改名为 beforeUnmount
  • destroyed 改名为 unmounted

4. 模板语法

Vue 3

  • 支持多根节点组件(Fragment)
  • 支持 v-memo 指令,用于缓存模板片段
  • 支持 v-bind 的简写形式 .

5. 性能优化

Vue 3

  • 更高效的响应式系统
  • 虚拟 DOM 重写,减少 patch 操作
  • 静态提升,缓存静态节点
  • 按需编译,减少运行时体积
  • 树摇优化,减少打包体积

6. TypeScript 支持

Vue 3

  • 原生支持 TypeScript
  • 更好的类型推断
  • 组合式 API 提供更好的类型支持

7. 其他改进

Vue 3

  • 更好的错误处理
  • 支持 Suspense 组件,用于处理异步组件
  • 支持 Teleport 组件,用于将组件内容渲染到指定位置
  • 支持 Fragment,允许组件有多个根节点
  • 改进的自定义指令 API
  • 改进的 provide/inject API

总结

Vue 3 带来了许多新特性和改进,包括更高效的响应式系统、组合式 API、更好的 TypeScript 支持等。这些改进使得 Vue 3 在性能、可维护性和开发体验方面都有显著提升。然而,Vue 3 也带来了一些破坏性变更,需要开发者进行相应的迁移。

11. vue2和vue3的响应式原理

Details

Vue 的响应式原理是其核心特性之一,Vue 2 和 Vue 3 在实现响应式系统方面有很大的不同。

Vue 2 的响应式原理

Vue 2 使用 Object.defineProperty() 方法对数据对象的属性进行劫持,当属性被访问或修改时,会触发相应的 getter 和 setter 方法。

基本实现

javascript
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集依赖
      Dep.target && dep.addSub(Dep.target);
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 通知更新
      dep.notify();
    }
  });
}

依赖收集

Vue 2 使用 Dep 类来管理依赖:

javascript
class Dep {
  constructor() {
    this.subs = [];
  }
  
  addSub(sub) {
    this.subs.push(sub);
  }
  
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

观察者

Vue 2 使用 Watcher 类来观察数据变化:

javascript
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.cb = cb;
    this.value = this.get();
  }
  
  get() {
    Dep.target = this;
    const value = this.expOrFn.call(this.vm);
    Dep.target = null;
    return value;
  }
  
  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

局限性

  1. 无法监听对象的新增属性:需要使用 Vue.set()this.$set() 方法
  2. 无法监听对象的删除属性:需要使用 Vue.delete()this.$delete() 方法
  3. 无法监听数组的索引和长度变化:需要使用数组的变异方法(如 push、pop、splice 等)
  4. 性能问题:在大型对象中,遍历所有属性并添加 getter 和 setter 会影响性能

Vue 3 的响应式原理

Vue 3 使用 Proxy 对象对数据对象进行代理,相比 Object.defineProperty()Proxy 可以监听对象的所有属性,包括新增属性和删除属性。

基本实现

javascript
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      // 收集依赖
      track(target, key);
      // 递归处理嵌套对象
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }
      return result;
    },
    set(target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver);
      const result = Reflect.set(target, key, value, receiver);
      // 通知更新
      if (oldValue !== value) {
        trigger(target, key, value, oldValue);
      }
      return result;
    },
    deleteProperty(target, key) {
      const hasKey = Reflect.has(target, key);
      const result = Reflect.deleteProperty(target, key);
      // 通知更新
      if (hasKey) {
        trigger(target, key, undefined, Reflect.get(target, key, receiver));
      }
      return result;
    }
  });
}

依赖收集和通知

Vue 3 使用 WeakMapMap 来管理依赖:

javascript
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

function trigger(target, key, value, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    const effects = new Set(dep);
    effects.forEach(effect => effect());
  }
}

优势

  1. 可以监听对象的新增属性:无需使用 Vue.set() 方法
  2. 可以监听对象的删除属性:无需使用 Vue.delete() 方法
  3. 可以监听数组的索引和长度变化:无需使用数组的变异方法
  4. 性能更好Proxy 是浏览器原生支持的,性能比 Object.defineProperty() 更好
  5. 代码更简洁Proxy 可以监听整个对象,而不需要遍历所有属性

总结

Vue 2 使用 Object.defineProperty() 实现响应式,存在一些局限性;Vue 3 使用 Proxy 实现响应式,解决了这些局限性,并且性能更好。两种实现方式各有优缺点,但 Vue 3 的响应式系统在功能和性能方面都有显著提升。

12. vue2和vue3的diff算法

Details

Vue 的 diff 算法是其虚拟 DOM 系统的核心,用于比较新旧虚拟 DOM 树的差异并高效更新真实 DOM。Vue 2 和 Vue 3 在 diff 算法的实现上有一些不同。

Vue 2 的 diff 算法

Vue 2 的 diff 算法基于以下原则:

  1. 同层比较:只比较同一层级的节点,不跨层级比较
  2. 先序遍历:按先序遍历的顺序比较节点
  3. key 匹配:使用 key 来匹配新旧节点

基本流程

  1. 初始化:创建新旧虚拟 DOM 树
  2. 比较根节点:如果根节点类型不同,直接替换整个子树
  3. 比较子节点
    • 如果新旧节点都有子节点,进行子节点比较
    • 如果只有新节点有子节点,添加新节点
    • 如果只有旧节点有子节点,删除旧节点
  4. 子节点比较
    • 使用双指针法,分别从新旧子节点的头部和尾部开始比较
    • 找到可复用的节点,进行更新
    • 处理剩余的节点,添加或删除

局限性

  1. 性能问题:在大型列表中,当节点位置发生变化时,diff 算法的性能会下降
  2. key 依赖:过度依赖 key 来匹配节点,如果 key 不稳定,会导致大量的 DOM 操作

Vue 3 的 diff 算法

Vue 3 的 diff 算法在 Vue 2 的基础上进行了优化,主要包括以下改进:

  1. 静态提升:缓存静态节点,避免重复比较
  2. 补丁标志:为节点添加补丁标志,只比较需要更新的部分
  3. 最长递增子序列:使用最长递增子序列算法来优化列表更新

基本流程

  1. 初始化:创建新旧虚拟 DOM 树
  2. 静态提升:识别并缓存静态节点
  3. 比较根节点:如果根节点类型不同,直接替换整个子树
  4. 比较子节点
    • 如果新旧节点都有子节点,进行子节点比较
    • 如果只有新节点有子节点,添加新节点
    • 如果只有旧节点有子节点,删除旧节点
  5. 子节点比较
    • 处理静态节点,直接复用
    • 使用补丁标志,只比较需要更新的部分
    • 对于列表,使用最长递增子序列算法来优化更新

优势

  1. 性能提升:通过静态提升和补丁标志,减少了不必要的比较和 DOM 操作
  2. 更高效的列表更新:使用最长递增子序列算法,减少了节点的移动操作
  3. 更好的类型支持:TypeScript 类型定义更完善

主要区别

特性Vue 2Vue 3
静态提升
补丁标志
列表更新算法双指针法最长递增子序列
性能一般更好
代码复杂度较低较高

总结

Vue 3 的 diff 算法在 Vue 2 的基础上进行了优化,通过静态提升、补丁标志和最长递增子序列算法等技术,显著提高了虚拟 DOM 的更新性能。这些优化使得 Vue 3 在处理大型应用和复杂列表时表现更好。

13. vue2和vue3的虚拟dom

Details

虚拟 DOM 是 Vue 的核心概念之一,它是对真实 DOM 的抽象,用于提高 DOM 更新的性能。Vue 2 和 Vue 3 在虚拟 DOM 的实现上有一些不同。

Vue 2 的虚拟 DOM

Vue 2 的虚拟 DOM 是基于 Snabbdom 实现的,主要包括以下特点:

  1. 节点类型

    • 元素节点:表示 HTML 元素
    • 文本节点:表示文本内容
    • 注释节点:表示注释
  2. 节点结构

    javascript
    {
      tag: 'div',           // 标签名
      data: {},            // 节点数据,如属性、事件等
      children: [],         // 子节点
      text: undefined,      // 文本内容
      elm: undefined,       // 对应的真实 DOM 元素
      ns: undefined,        // 命名空间
      context: undefined,   // 组件实例
      functionalContext: undefined, // 函数式组件上下文
      key: undefined,       // 节点 key
      componentOptions: undefined, // 组件选项
      componentInstance: undefined, // 组件实例
      parent: undefined,    // 父节点
      raw: false,           // 是否为原始 HTML
      isStatic: false,      // 是否为静态节点
      isRootInsert: true,   // 是否为根节点插入
      isComment: false,     // 是否为注释节点
      isCloned: false,      // 是否为克隆节点
      isOnce: false,        // 是否为 v-once 节点
    }
  3. 渲染过程

    • 模板编译:将模板编译为渲染函数
    • 渲染函数执行:生成虚拟 DOM 树
    • diff 算法:比较新旧虚拟 DOM 树的差异
    • 补丁操作:根据差异更新真实 DOM

Vue 3 的虚拟 DOM

Vue 3 的虚拟 DOM 在 Vue 2 的基础上进行了优化,主要包括以下改进:

  1. 节点类型

    • 元素节点:表示 HTML 元素
    • 文本节点:表示文本内容
    • 注释节点:表示注释
    • 静态节点:表示静态内容
    • 片段节点:表示多个根节点
  2. 节点结构

    javascript
    {
      type: Symbol(Fragment), // 节点类型
      props: {},             // 节点属性
      children: [],           // 子节点
      patchFlag: 0,           // 补丁标志
      dynamicProps: null,     // 动态属性
      dynamicChildren: null,  // 动态子节点
    }
  3. 优化

    • 静态提升:缓存静态节点,避免重复比较
    • 补丁标志:为节点添加补丁标志,只比较需要更新的部分
    • hoisting:将静态节点提升到渲染函数之外,避免重复创建
    • cacheHandler:缓存事件处理器,避免重复创建
    • SSR 优化:更好的服务端渲染支持
  4. 渲染过程

    • 模板编译:将模板编译为渲染函数,添加静态提升、补丁标志等优化
    • 渲染函数执行:生成虚拟 DOM 树
    • diff 算法:使用优化后的 diff 算法比较新旧虚拟 DOM 树的差异
    • 补丁操作:根据差异更新真实 DOM

主要区别

特性Vue 2Vue 3
节点结构复杂简洁
静态提升
补丁标志
片段节点
性能一般更好
代码复杂度较低较高

总结

Vue 3 的虚拟 DOM 在 Vue 2 的基础上进行了优化,通过静态提升、补丁标志等技术,显著提高了虚拟 DOM 的更新性能。这些优化使得 Vue 3 在处理大型应用和复杂列表时表现更好。

14. vue2和vue3的组件通信

Details

组件通信是 Vue 应用中的重要部分,Vue 2 和 Vue 3 在组件通信方式上有一些相同之处,也有一些不同之处。

1. Props / Emit

Vue 2

javascript
// 父组件
<template>
  <child :message="message" @update="handleUpdate" />
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello'
    };
  },
  methods: {
    handleUpdate(newMessage) {
      this.message = newMessage;
    }
  }
};
</script>

// 子组件
<template>
  <div>{{ message }}</div>
  <button @click="updateMessage">Update</button>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      default: ''
    }
  },
  methods: {
    updateMessage() {
      this.$emit('update', 'Hello from child');
    }
  }
};
</script>

Vue 3

javascript
// 父组件
<template>
  <child :message="message" @update="handleUpdate" />
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

const message = ref('Hello');

function handleUpdate(newMessage) {
  message.value = newMessage;
}
</script>

// 子组件
<template>
  <div>{{ message }}</div>
  <button @click="updateMessage">Update</button>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  message: {
    type: String,
    default: ''
  }
});

const emit = defineEmits(['update']);

function updateMessage() {
  emit('update', 'Hello from child');
}
</script>

2. $parent / $children

Vue 2

javascript
// 父组件访问子组件
this.$children[0].method();

// 子组件访问父组件
this.$parent.method();

Vue 3: Vue 3 中不再推荐使用 $parent$children,而是使用 provide / inject 或 ref。

3. provide / inject

Vue 2

javascript
// 父组件
export default {
  provide() {
    return {
      message: 'Hello from parent'
    };
  }
};

// 子组件
export default {
  inject: ['message']
};

Vue 3

javascript
// 父组件
<script setup>
import { provide } from 'vue';

provide('message', 'Hello from parent');
</script>

// 子组件
<script setup>
import { inject } from 'vue';

const message = inject('message');
</script>

4. ref / $refs

Vue 2

javascript
// 父组件
<template>
  <child ref="childRef" />
</template>

<script>
export default {
  mounted() {
    this.$refs.childRef.method();
  }
};
</script>

Vue 3

javascript
// 父组件
<template>
  <child ref="childRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue';
import Child from './Child.vue';

const childRef = ref(null);

onMounted(() => {
  childRef.value.method();
});
</script>

5. Event Bus

Vue 2

javascript
// 创建事件总线
const bus = new Vue();

// 发送事件
bus.$emit('event', data);

// 监听事件
bus.$on('event', data => {
  console.log(data);
});

Vue 3: Vue 3 中不再推荐使用 Event Bus,而是使用 mitt 或 tiny-emitter 等第三方库。

javascript
// 安装 mitt
// npm install mitt

// 创建事件总线
import mitt from 'mitt';
const bus = mitt();

// 发送事件
bus.emit('event', data);

// 监听事件
bus.on('event', data => {
  console.log(data);
});

6. Vuex

Vue 2

javascript
// 安装 Vuex
// npm install vuex

// 创建 store
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
});

// 在组件中使用
this.$store.state.count;
this.$store.commit('increment');
this.$store.dispatch('incrementAsync');
this.$store.getters.doubleCount;

Vue 3

javascript
// 安装 Vuex
// npm install vuex@next

// 创建 store
import { createStore } from 'vuex';

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment');
      }, 1000);
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
});

// 在组件中使用
import { useStore } from 'vuex';

const store = useStore();
store.state.count;
store.commit('increment');
store.dispatch('incrementAsync');
store.getters.doubleCount;

7. Pinia

Vue 3: Vue 3 推荐使用 Pinia 作为状态管理库,它是 Vuex 的继任者,提供了更好的 TypeScript 支持和更简洁的 API。

javascript
// 安装 Pinia
// npm install pinia

// 创建 store
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++;
    },
    incrementAsync() {
      setTimeout(() => {
        this.count++;
      }, 1000);
    }
  }
});

// 在组件中使用
import { useCounterStore } from './stores/counter';

const counterStore = useCounterStore();
counterStore.count;
counterStore.increment();
counterStore.incrementAsync();
counterStore.doubleCount;

总结

Vue 2 和 Vue 3 在组件通信方式上有一些相同之处,如 Props / Emit、provide / inject、ref / $refs 等,也有一些不同之处,如 Vue 3 不再推荐使用 $parent / $children 和 Event Bus,而是推荐使用 provide / inject 或 Pinia 等。在实际开发中,应根据具体场景选择合适的组件通信方式。

15. vue2和vue3的keep-alive

Details

keep-alive 是 Vue 中的一个内置组件,用于缓存组件状态,避免重复渲染。Vue 2 和 Vue 3 在 keep-alive 的使用方式上有一些相同之处,也有一些不同之处。

Vue 2 的 keep-alive

基本使用

html
<keep-alive>
  <component :is="currentComponent"></component>
</keep-alive>

属性

  • include:字符串或正则表达式,只有名称匹配的组件会被缓存
  • exclude:字符串或正则表达式,名称匹配的组件不会被缓存
  • max:数字,最多可以缓存多少个组件实例

示例

html
<keep-alive include="Home,About">
  <component :is="currentComponent"></component>
</keep-alive>

<keep-alive :max="10">
  <component :is="currentComponent"></component>
</keep-alive>

生命周期钩子

  • activated:组件被激活时调用
  • deactivated:组件被停用时调用

示例

javascript
export default {
  activated() {
    console.log('Component activated');
  },
  deactivated() {
    console.log('Component deactivated');
  }
};

Vue 3 的 keep-alive

基本使用

html
<keep-alive>
  <component :is="currentComponent"></component>
</keep-alive>

属性

  • include:字符串、正则表达式或数组,只有名称匹配的组件会被缓存
  • exclude:字符串、正则表达式或数组,名称匹配的组件不会被缓存
  • max:数字,最多可以缓存多少个组件实例

示例

html
<keep-alive include="Home,About">
  <component :is="currentComponent"></component>
</keep-alive>

<keep-alive :include="['Home', 'About']">
  <component :is="currentComponent"></component>
</keep-alive>

<keep-alive :max="10">
  <component :is="currentComponent"></component>
</keep-alive>

生命周期钩子

  • onActivated:组件被激活时调用(组合式 API)
  • onDeactivated:组件被停用时调用(组合式 API)

示例

javascript
// 选项式 API
export default {
  activated() {
    console.log('Component activated');
  },
  deactivated() {
    console.log('Component deactivated');
  }
};

// 组合式 API
import { onActivated, onDeactivated } from 'vue';

export default {
  setup() {
    onActivated(() => {
      console.log('Component activated');
    });

    onDeactivated(() => {
      console.log('Component deactivated');
    });
  }
};

主要区别

特性Vue 2Vue 3
基本用法相同相同
include/exclude字符串或正则表达式字符串、正则表达式或数组
max相同相同
生命周期钩子activated/deactivated选项式 API 相同,组合式 API 使用 onActivated/onDeactivated
性能一般更好

总结

Vue 2 和 Vue 3 在 keep-alive 的使用方式上基本相同,但 Vue 3 对 includeexclude 属性进行了扩展,支持数组格式,并且在组合式 API 中提供了 onActivatedonDeactivated 生命周期钩子。此外,Vue 3 对 keep-alive 的性能也进行了优化,使其在缓存组件时更加高效。

16. Vue 3 的 Teleport 组件

Details

Teleport 是 Vue 3 中引入的一个新组件,用于将组件的内容渲染到 DOM 树中的指定位置,而不是组件的父组件内部。

基本使用

html
<template>
  <div class="container">
    <h1>Main Content</h1>
    <Teleport to="#modal-container">
      <div class="modal">
        <h2>Modal Content</h2>
        <p>This is rendered in the modal container</p>
      </div>
    </Teleport>
  </div>
</template>

<!-- 在 HTML 中 -->
<body>
  <div id="app"></div>
  <div id="modal-container"></div>
</body>

特性

  1. to 属性:指定内容要渲染到的目标位置,可以是 CSS 选择器或 DOM 元素。
  2. disabled 属性:控制是否禁用 Teleport,当设置为 true 时,内容会渲染在原来的位置。

示例

基本用法

html
<Teleport to="body">
  <div class="fixed-overlay">
    <p>Overlay content</p>
  </div>
</Teleport>

条件渲染

html
<Teleport to="#modal-container" :disabled="!showModal">
  <div v-if="showModal" class="modal">
    <h2>Modal</h2>
    <button @click="showModal = false">Close</button>
  </div>
</Teleport>

应用场景

  1. 模态框:将模态框内容渲染到 body 或其他指定容器,避免 z-index 问题。
  2. 通知组件:将通知消息渲染到页面顶部或其他固定位置。
  3. 悬浮组件:将悬浮内容渲染到指定位置,不受父组件样式影响。

注意事项

  1. 目标元素必须存在:Teleport 不会创建目标元素,只是将内容移动到已存在的元素中。
  2. 嵌套 Teleport:可以嵌套使用 Teleport,但要注意目标位置的正确性。
  3. 事件冒泡:Teleport 中的事件会正常冒泡到父组件,不受 teleport 位置的影响。

总结

Teleport 组件是 Vue 3 中一个非常实用的新特性,它允许我们将组件内容渲染到 DOM 树中的指定位置,解决了传统组件嵌套中的一些布局和样式问题。在实际开发中,Teleport 特别适合用于模态框、通知、悬浮组件等场景。

17. Vue 3 的 Suspense 组件

Details

Suspense 是 Vue 3 中引入的一个新组件,用于处理异步组件的加载状态。它可以在异步组件加载完成前显示一个加载状态,加载完成后显示组件内容。

基本使用

html
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  timeout: 3000 // 加载超时时间
});

export default {
  components: {
    AsyncComponent
  }
};
</script>

特性

  1. default 插槽:用于放置异步组件。
  2. fallback 插槽:用于在异步组件加载完成前显示的内容。
  3. 支持 await:可以在 setup() 函数中使用 await 来等待异步操作完成。

示例

使用 defineAsyncComponent

javascript
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingComponent, // 加载状态组件
  errorComponent: ErrorComponent, // 错误状态组件
  delay: 200, // 延迟显示加载状态的时间
  timeout: 3000, // 加载超时时间
  suspensible: true // 是否可挂起
});

在 setup() 中使用 await

javascript
// AsyncComponent.vue
<template>
  <div>
    <h2>{{ data.title }}</h2>
    <p>{{ data.description }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 模拟异步数据获取
const fetchData = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ 
        title: 'Async Data', 
        description: 'This data was fetched asynchronously' 
      });
    }, 2000);
  });
};

// 在 setup() 中使用 await
const data = await fetchData();
</script>

应用场景

  1. 异步组件加载:当组件需要从服务器加载时,可以使用 Suspense 显示加载状态。
  2. 数据预加载:当组件需要获取数据后才能渲染时,可以使用 Suspense 等待数据加载完成。
  3. 代码分割:结合 defineAsyncComponent 实现代码分割,提高应用加载速度。

注意事项

  1. 只支持异步组件Suspense 只能包裹异步组件或使用 await 的组件。
  2. 错误处理:需要单独处理异步操作的错误,可以使用 errorCapturedonErrorCaptured
  3. 嵌套使用:可以嵌套使用 Suspense 组件,但要注意性能影响。

总结

Suspense 组件是 Vue 3 中一个处理异步操作的新特性,它可以让我们更优雅地处理异步组件的加载状态,提高用户体验。在实际开发中,Suspense 特别适合用于需要加载异步数据或异步组件的场景。

18. Vue Router 的动态路由和嵌套路由

Details

Vue Router 是 Vue 官方的路由管理器,它支持动态路由和嵌套路由,使我们可以构建更复杂的单页应用。

动态路由

基本概念: 动态路由是指路由路径中包含动态参数的路由,这些参数可以在组件中通过 $route.params 访问。

配置示例

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/user/:id',
      component: User,
      props: true // 可以将路由参数作为 props 传递给组件
    }
  ]
});

使用示例

javascript
// User.vue
<template>
  <div>
    <h1>User {{ id }}</h1>
  </div>
</template>

<script>
export default {
  props: ['id'], // 通过 props 接收路由参数
  mounted() {
    // 也可以通过 $route.params 访问
    console.log(this.$route.params.id);
  }
};
</script>

路由参数变化: 当路由参数发生变化时,组件不会重新创建,而是会触发 beforeRouteUpdate 导航守卫:

javascript
export default {
  beforeRouteUpdate(to, from, next) {
    // 路由参数变化时的处理逻辑
    this.id = to.params.id;
    this.fetchData(this.id);
    next();
  }
};

嵌套路由

基本概念: 嵌套路由是指在一个路由中嵌套另一个路由,形成路由层级结构。

配置示例

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/user/:id',
      component: User,
      children: [
        {
          // 当 /user/:id/profile 匹配成功时,UserProfile 会被渲染在 User 的 <router-view> 中
          path: 'profile',
          component: UserProfile
        },
        {
          // 当 /user/:id/posts 匹配成功时,UserPosts 会被渲染在 User 的 <router-view> 中
          path: 'posts',
          component: UserPosts
        }
      ]
    }
  ]
});

使用示例

html
<!-- User.vue -->
<template>
  <div>
    <h1>User {{ id }}</h1>
    <router-view></router-view> <!-- 子路由会渲染在这里 -->
  </div>
</template>

路由守卫

全局守卫

javascript
// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 检查用户是否登录
  if (to.meta.requiresAuth && !isLoggedIn) {
    next('/login');
  } else {
    next();
  }
});

路由独享守卫

javascript
const router = new VueRouter({
  routes: [
    {
      path: '/admin',
      component: Admin,
      beforeEnter: (to, from, next) => {
        // 检查用户是否是管理员
        if (!isAdmin) {
          next('/');
        } else {
          next();
        }
      }
    }
  ]
});

组件内守卫

javascript
export default {
  beforeRouteEnter(to, from, next) {
    // 在组件渲染前调用
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由更新时调用
  },
  beforeRouteLeave(to, from, next) {
    // 在离开当前路由时调用
  }
};

总结

Vue Router 的动态路由和嵌套路由是构建复杂单页应用的重要特性。动态路由允许我们处理参数化的路由,嵌套路由允许我们构建有层级结构的路由系统。结合路由守卫,我们可以实现更灵活、更安全的路由控制。

19. Pinia 和 Vuex 的区别

Details

Pinia 是 Vue 3 推荐的状态管理库,它是 Vuex 的继任者,提供了更好的 TypeScript 支持和更简洁的 API。

Pinia 的特点

  1. 简洁的 API:Pinia 的 API 更加简洁,不需要 mutations,直接在 actions 中修改状态。
  2. 更好的 TypeScript 支持:Pinia 原生支持 TypeScript,类型推断更加准确。
  3. 模块化设计:Pinia 的 store 是模块化的,每个 store 都是一个独立的模块。
  4. 无需命名空间:Pinia 的 store 自动具有命名空间,无需手动设置。
  5. 支持组合式 API:Pinia 可以与 Vue 3 的组合式 API 无缝集成。
  6. 轻量级:Pinia 的体积很小,只有约 1kb。

基本使用

创建 store

javascript
// stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Pinia'
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    // 可以访问其他 getter
    doubleCountPlusOne: (state, getters) => getters.doubleCount + 1
  },
  actions: {
    increment() {
      this.count++;
    },
    // 支持异步操作
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000));
      this.count++;
    }
  }
});

在组件中使用

javascript
// 选项式 API
import { useCounterStore } from '@/stores/counter';

export default {
  computed: {
    ...mapState(useCounterStore, ['count', 'name']),
    ...mapGetters(useCounterStore, ['doubleCount'])
  },
  methods: {
    ...mapActions(useCounterStore, ['increment', 'incrementAsync'])
  }
};

// 组合式 API
import { useCounterStore } from '@/stores/counter';
import { computed } from 'vue';

export default {
  setup() {
    const counterStore = useCounterStore();
    
    const count = computed(() => counterStore.count);
    const doubleCount = computed(() => counterStore.doubleCount);
    
    function increment() {
      counterStore.increment();
    }
    
    function incrementAsync() {
      counterStore.incrementAsync();
    }
    
    return {
      count,
      doubleCount,
      increment,
      incrementAsync
    };
  }
};

与 Vuex 的区别

特性VuexPinia
Mutations必须使用 mutations 修改状态无需 mutations,直接在 actions 中修改状态
TypeScript 支持较差,需要手动添加类型原生支持 TypeScript,类型推断准确
模块化需要手动设置命名空间自动具有命名空间
组合式 API 支持较差良好
体积较大较小(约 1kb)
调试工具Vue DevTools支持 Vue DevTools
生态系统成熟正在发展中

迁移策略

如果您正在从 Vuex 迁移到 Pinia,可以按照以下步骤进行:

  1. 安装 Pinianpm install pinia
  2. 创建 Pinia 实例:在 main.js 中创建并使用 Pinia
  3. 迁移 store:将 Vuex 的 store 转换为 Pinia 的 store
  4. 更新组件:将组件中的 Vuex 代码更新为 Pinia 代码
  5. 测试:确保应用正常运行

总结

Pinia 是 Vue 3 推荐的状态管理库,它提供了更简洁的 API、更好的 TypeScript 支持和更灵活的模块化设计。与 Vuex 相比,Pinia 更加轻量级,使用更加简单,是构建现代 Vue 应用的理想选择。

20. Vue 中的自定义指令

Details

Vue 允许我们创建自定义指令,用于对 DOM 元素进行底层操作。自定义指令可以在组件的模板中使用,就像内置指令一样。

基本语法

全局指令

javascript
// 注册全局指令
Vue.directive('focus', {
  // 指令绑定到元素时调用
  bind(el, binding, vnode) {
    // 做一次性的初始化设置
  },
  // 元素插入到 DOM 中时调用
  inserted(el, binding, vnode) {
    // 例如:聚焦元素
    el.focus();
  },
  // 组件更新时调用
  update(el, binding, vnode, oldVnode) {
    // 根据绑定值的变化更新元素
  },
  // 组件更新完成后调用
  componentUpdated(el, binding, vnode, oldVnode) {
    // 组件更新完成后的操作
  },
  // 指令与元素解绑时调用
  unbind(el, binding, vnode) {
    // 清理工作
  }
});

局部指令

javascript
export default {
  directives: {
    focus: {
      // 指令定义
      inserted(el) {
        el.focus();
      }
    }
  }
};

指令钩子函数

  1. bind:指令绑定到元素时调用,只执行一次。
  2. inserted:元素插入到 DOM 中时调用。
  3. update:组件更新时调用,可能在子组件更新之前。
  4. componentUpdated:组件更新完成后调用。
  5. unbind:指令与元素解绑时调用,只执行一次。

钩子函数参数

  1. el:指令绑定的元素,可以直接操作 DOM。
  2. binding:一个对象,包含以下属性:
    • name:指令名称,不包括 v- 前缀。
    • value:指令的绑定值。
    • oldValue:指令的前一个绑定值,仅在 update 和 componentUpdated 钩子中可用。
    • expression:指令的表达式。
    • arg:指令的参数。
    • modifiers:指令的修饰符。
  3. vnode:Vue 编译生成的虚拟节点。
  4. oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

示例

自定义指令 v-focus

javascript
// 全局注册
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

// 在模板中使用
<template>
  <input v-focus type="text">
</template>

自定义指令 v-color

javascript
// 全局注册
Vue.directive('color', {
  bind(el, binding) {
    el.style.color = binding.value;
  },
  update(el, binding) {
    el.style.color = binding.value;
  }
});

// 在模板中使用
<template>
  <div v-color="color">Hello World</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    };
  }
};
</script>

带参数的自定义指令

javascript
// 全局注册
Vue.directive('position', {
  bind(el, binding) {
    el.style.position = 'fixed';
    el.style[binding.arg] = binding.value + 'px';
  }
});

// 在模板中使用
<template>
  <div v-position:top="100">Hello World</div>
</template>

应用场景

  1. 表单元素聚焦:自动聚焦到输入框。
  2. 滚动到底部:聊天应用中自动滚动到最新消息。
  3. 拖拽功能:实现元素的拖拽功能。
  4. 图片懒加载:延迟加载图片。
  5. 权限控制:根据用户权限显示或隐藏元素。

总结

自定义指令是 Vue 中一个强大的特性,它允许我们对 DOM 元素进行底层操作,实现一些内置指令无法实现的功能。在实际开发中,自定义指令可以帮助我们封装一些常用的 DOM 操作,提高代码的复用性和可维护性。

21. Vue 中的插槽(Slot)

Details

插槽是 Vue 中用于组件内容分发的机制,它允许我们在父组件中向子组件传递内容,实现组件的复用和定制。

基本插槽

子组件

html
<!-- Child.vue -->
<template>
  <div class="child">
    <h2>Child Component</h2>
    <slot></slot> <!-- 插槽位置 -->
  </div>
</template>

父组件

html
<!-- Parent.vue -->
<template>
  <div class="parent">
    <h1>Parent Component</h1>
    <Child>
      <p>This is content passed to the slot</p>
    </Child>
  </div>
</template>

<script>
import Child from './Child.vue';

export default {
  components: {
    Child
  }
};
</script>

具名插槽

子组件

html
<!-- Child.vue -->
<template>
  <div class="child">
    <header>
      <slot name="header"></slot> <!-- 具名插槽 -->
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot> <!-- 具名插槽 -->
    </footer>
  </div>
</template>

父组件

html
<!-- Parent.vue -->
<template>
  <div class="parent">
    <h1>Parent Component</h1>
    <Child>
      <template v-slot:header>
        <h2>Header Content</h2>
      </template>
      <p>Main Content</p>
      <template v-slot:footer>
        <p>Footer Content</p>
      </template>
    </Child>
  </div>
</template>

<script>
import Child from './Child.vue';

export default {
  components: {
    Child
  }
};
</script>

缩写语法

html
<Child>
  <template #header>
    <h2>Header Content</h2>
  </template>
  <p>Main Content</p>
  <template #footer>
    <p>Footer Content</p>
  </template>
</Child>

作用域插槽

子组件

html
<!-- Child.vue -->
<template>
  <div class="child">
    <h2>Child Component</h2>
    <ul>
      <li v-for="item in items" :key="item.id">
        <slot :item="item">{{ item.name }}</slot> <!-- 作用域插槽,传递 item 数据 -->
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    };
  }
};
</script>

父组件

html
<!-- Parent.vue -->
<template>
  <div class="parent">
    <h1>Parent Component</h1>
    <Child>
      <template v-slot:default="slotProps">
        <!-- 使用 slotProps 访问子组件传递的数据 -->
        {{ slotProps.item.id }}: {{ slotProps.item.name }}
      </template>
    </Child>
  </div>
</template>

<script>
import Child from './Child.vue';

export default {
  components: {
    Child
  }
};
</script>

解构语法

html
<Child>
  <template v-slot:default="{ item }">
    <!-- 解构 slotProps -->
    {{ item.id }}: {{ item.name }}
  </template>
</Child>

动态插槽名

父组件

html
<template>
  <div class="parent">
    <h1>Parent Component</h1>
    <Child>
      <template v-slot:[dynamicSlotName]>
        <p>Dynamic Slot Content</p>
      </template>
    </Child>
  </div>
</template>

<script>
export default {
  data() {
    return {
      dynamicSlotName: 'header'
    };
  },
  components: {
    Child
  }
};
</script>

应用场景

  1. 布局组件:创建可定制的布局组件,如页面头部、主体、底部。
  2. 列表组件:创建可定制的列表项组件。
  3. 卡片组件:创建可定制的卡片组件,如卡片头部、内容、底部。
  4. 表单组件:创建可定制的表单组件,如表单字段标签、输入框、错误信息。

总结

插槽是 Vue 中一个强大的特性,它允许我们在父组件中向子组件传递内容,实现组件的复用和定制。通过基本插槽、具名插槽和作用域插槽,我们可以创建更加灵活和可复用的组件。

22. Vue 中的异步组件

Details

异步组件是 Vue 中用于延迟加载组件的机制,它可以减少初始加载时间,提高应用性能。

基本用法

Vue 2

javascript
// 全局注册
Vue.component('AsyncComponent', function(resolve, reject) {
  // 异步加载组件
  setTimeout(() => {
    resolve({
      template: '<div>Async Component</div>'
    });
  }, 1000);
});

// 局部注册
export default {
  components: {
    AsyncComponent: function(resolve, reject) {
      setTimeout(() => {
        resolve({
          template: '<div>Async Component</div>'
        });
      }, 1000);
    }
  }
};

Vue 3

javascript
import { defineAsyncComponent } from 'vue';

// 基本用法
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  timeout: 3000 // 加载超时时间
});

// 简化用法
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));

// 局部注册
export default {
  components: {
    AsyncComponent
  }
};

高级选项

Vue 3

javascript
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingComponent, // 加载状态组件
  errorComponent: ErrorComponent, // 错误状态组件
  delay: 200, // 延迟显示加载状态的时间(毫秒)
  timeout: 3000, // 加载超时时间(毫秒)
  suspensible: true, // 是否可挂起,与 Suspense 组件配合使用
  onError(error, retry, fail, attempts) {
    // 错误处理函数
    if (attempts <= 3) {
      // 最多重试 3 次
      retry();
    } else {
      fail();
    }
  }
});

与 Suspense 配合使用

Vue 3

html
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));

export default {
  components: {
    AsyncComponent
  }
};
</script>

应用场景

  1. 大型组件:延迟加载体积较大的组件,如地图、图表等。
  2. 路由懒加载:与 Vue Router 配合使用,实现路由的懒加载。
  3. 条件渲染:只在需要时加载组件,如用户点击某个按钮后才加载的组件。
  4. 代码分割:将应用代码分割成多个小块,减少初始加载时间。

路由懒加载示例

javascript
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: () => import('./views/Home.vue')
    },
    {
      path: '/about',
      component: () => import('./views/About.vue')
    },
    {
      path: '/user/:id',
      component: () => import('./views/User.vue')
    }
  ]
});

export default router;

总结

异步组件是 Vue 中用于延迟加载组件的机制,它可以减少初始加载时间,提高应用性能。在 Vue 3 中,defineAsyncComponent 函数提供了更灵活的配置选项,与 Suspense 组件配合使用可以实现更优雅的加载状态管理。

23. Vue 中的依赖注入(provide/inject)

Details

依赖注入是 Vue 中用于组件间通信的一种机制,它允许父组件向其所有子组件(包括深层子组件)提供数据和方法,而无需通过 props 层层传递。

基本用法

父组件

javascript
// Vue 2
export default {
  provide() {
    return {
      message: 'Hello from parent',
      method: this.someMethod
    };
  },
  methods: {
    someMethod() {
      console.log('Method called from parent');
    }
  }
};

// Vue 3 - 选项式 API
export default {
  provide() {
    return {
      message: 'Hello from parent',
      method: this.someMethod
    };
  },
  methods: {
    someMethod() {
      console.log('Method called from parent');
    }
  }
};

// Vue 3 - 组合式 API
import { provide } from 'vue';

export default {
  setup() {
    const message = 'Hello from parent';
    
    function someMethod() {
      console.log('Method called from parent');
    }
    
    provide('message', message);
    provide('method', someMethod);
  }
};

子组件

javascript
// Vue 2
export default {
  inject: ['message', 'method'],
  mounted() {
    console.log(this.message); // 输出: Hello from parent
    this.method(); // 输出: Method called from parent
  }
};

// Vue 3 - 选项式 API
export default {
  inject: ['message', 'method'],
  mounted() {
    console.log(this.message); // 输出: Hello from parent
    this.method(); // 输出: Method called from parent
  }
};

// Vue 3 - 组合式 API
import { inject } from 'vue';

export default {
  setup() {
    const message = inject('message');
    const method = inject('method');
    
    console.log(message); // 输出: Hello from parent
    method(); // 输出: Method called from parent
    
    return {
      message
    };
  }
};

带默认值的注入

子组件

javascript
// Vue 2
export default {
  inject: {
    message: {
      default: 'Default message'
    },
    method: {
      default: function() {
        console.log('Default method');
      }
    }
  }
};

// Vue 3 - 选项式 API
export default {
  inject: {
    message: {
      default: 'Default message'
    },
    method: {
      default: function() {
        console.log('Default method');
      }
    }
  }
};

// Vue 3 - 组合式 API
import { inject } from 'vue';

export default {
  setup() {
    const message = inject('message', 'Default message');
    const method = inject('method', () => {
      console.log('Default method');
    });
    
    return {
      message
    };
  }
};

响应式注入

父组件

javascript
// Vue 3 - 组合式 API
import { provide, ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    
    function increment() {
      count.value++;
    }
    
    provide('count', count);
    provide('increment', increment);
    
    return {
      count,
      increment
    };
  }
};

子组件

javascript
// Vue 3 - 组合式 API
import { inject } from 'vue';

export default {
  setup() {
    const count = inject('count');
    const increment = inject('increment');
    
    return {
      count,
      increment
    };
  }
};

应用场景

  1. 主题配置:在应用根组件中提供主题配置,所有子组件都可以访问。
  2. 用户信息:在应用根组件中提供用户信息,所有子组件都可以访问。
  3. 全局状态:在应用根组件中提供全局状态,所有子组件都可以访问。
  4. 服务:在应用根组件中提供服务(如 API 服务),所有子组件都可以使用。

注意事项

  1. 响应式:如果提供的值是响应式的(如 ref 或 reactive),那么注入的值也是响应式的。
  2. 作用域:provide/inject 只在父组件及其子组件之间有效,不影响其他组件树。
  3. 命名冲突:如果多个父组件提供了相同名称的注入,子组件会接收最近的父组件提供的值。
  4. 类型安全:在 TypeScript 中,需要为注入的值添加类型注解。

总结

依赖注入是 Vue 中用于组件间通信的一种机制,它允许父组件向其所有子组件提供数据和方法,而无需通过 props 层层传递。在 Vue 3 中,provide/inject API 与组合式 API 无缝集成,使用更加灵活。

24. Vue 中的错误处理

Details

错误处理是 Vue 应用中重要的一环,它可以帮助我们捕获和处理应用中的错误,提高应用的稳定性和用户体验。

错误处理策略

  1. 全局错误处理:捕获整个应用的错误。
  2. 组件级错误处理:捕获特定组件的错误。
  3. 异步错误处理:捕获异步操作(如 API 请求)的错误。
  4. 路由错误处理:捕获路由导航过程中的错误。

全局错误处理

Vue 2

javascript
// 全局错误处理
Vue.config.errorHandler = function(err, vm, info) {
  console.error('Vue 错误:', err);
  console.error('组件:', vm);
  console.error('错误信息:', info);
  // 可以在这里添加错误上报逻辑
};

// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', function(event) {
  console.error('未处理的 Promise 错误:', event.reason);
  // 可以在这里添加错误上报逻辑
});

// 未捕获的错误
window.addEventListener('error', function(event) {
  console.error('未捕获的错误:', event.error);
  // 可以在这里添加错误上报逻辑
});

Vue 3

javascript
// 全局错误处理
app.config.errorHandler = function(err, instance, info) {
  console.error('Vue 错误:', err);
  console.error('组件实例:', instance);
  console.error('错误信息:', info);
  // 可以在这里添加错误上报逻辑
};

// 未捕获的 Promise 错误
window.addEventListener('unhandledrejection', function(event) {
  console.error('未处理的 Promise 错误:', event.reason);
  // 可以在这里添加错误上报逻辑
});

// 未捕获的错误
window.addEventListener('error', function(event) {
  console.error('未捕获的错误:', event.error);
  // 可以在这里添加错误上报逻辑
});

组件级错误处理

Vue 2

javascript
export default {
  errorCaptured(err, vm, info) {
    console.error('组件错误:', err);
    console.error('子组件:', vm);
    console.error('错误信息:', info);
    // 可以在这里添加错误处理逻辑
    // 返回 true 可以阻止错误继续向上传播
    return false;
  }
};

Vue 3 - 选项式 API

javascript
export default {
  errorCaptured(err, instance, info) {
    console.error('组件错误:', err);
    console.error('子组件实例:', instance);
    console.error('错误信息:', info);
    // 可以在这里添加错误处理逻辑
    // 返回 true 可以阻止错误继续向上传播
    return false;
  }
};

Vue 3 - 组合式 API

javascript
import { onErrorCaptured } from 'vue';

export default {
  setup() {
    onErrorCaptured((err, instance, info) => {
      console.error('组件错误:', err);
      console.error('子组件实例:', instance);
      console.error('错误信息:', info);
      // 可以在这里添加错误处理逻辑
      // 返回 true 可以阻止错误继续向上传播
      return false;
    });
  }
};

异步错误处理

使用 try/catch

javascript
export default {
  methods: {
    async fetchData() {
      try {
        const response = await axios.get('/api/data');
        this.data = response.data;
      } catch (error) {
        console.error('API 请求错误:', error);
        this.error = '获取数据失败,请重试';
      }
    }
  }
};

使用 Promise.catch

javascript
export default {
  methods: {
    fetchData() {
      axios.get('/api/data')
        .then(response => {
          this.data = response.data;
        })
        .catch(error => {
          console.error('API 请求错误:', error);
          this.error = '获取数据失败,请重试';
        });
    }
  }
};

路由错误处理

Vue Router

javascript
const router = createRouter({
  history: createWebHistory(),
  routes
});

// 路由错误处理
router.onError(error => {
  console.error('路由错误:', error);
  // 可以在这里添加错误处理逻辑
});

export default router;

错误边界组件

Vue 3

javascript
// ErrorBoundary.vue
<template>
  <div v-if="error" class="error-boundary">
    <h2>发生错误</h2>
    <p>{{ error.message }}</p>
    <button @click="resetError">重试</button>
  </div>
  <slot v-else></slot>
</template>

<script>
import { ref, onErrorCaptured } from 'vue';

export default {
  setup(props, { slots }) {
    const error = ref(null);
    
    const resetError = () => {
      error.value = null;
    };
    
    onErrorCaptured((err) => {
      error.value = err;
      return true; // 阻止错误继续向上传播
    });
    
    return {
      error,
      resetError
    };
  }
};
</script>

// 使用
<template>
  <ErrorBoundary>
    <ComponentThatMightError />
  </ErrorBoundary>
</template>

<script>
import ErrorBoundary from './ErrorBoundary.vue';
import ComponentThatMightError from './ComponentThatMightError.vue';

export default {
  components: {
    ErrorBoundary,
    ComponentThatMightError
  }
};
</script>

总结

错误处理是 Vue 应用中重要的一环,它可以帮助我们捕获和处理应用中的错误,提高应用的稳定性和用户体验。通过全局错误处理、组件级错误处理、异步错误处理和路由错误处理,我们可以构建更加健壮的 Vue 应用。

25. Vue 3 的 Fragment 组件

Details

Fragment 是 Vue 3 中引入的一个新特性,它允许组件拥有多个根节点,而不需要像 Vue 2 那样只能有一个根节点。

基本使用

Vue 2(只能有一个根节点):

html
<template>
  <div> <!-- 必须有一个根节点 -->
    <h1>Hello</h1>
    <p>World</p>
  </div>
</template>

Vue 3(可以有多个根节点):

html
<template>
  <h1>Hello</h1> <!-- 第一个根节点 -->
  <p>World</p> <!-- 第二个根节点 -->
</template>

优势

  1. 减少不必要的 DOM 元素:不需要为了满足单根节点的要求而添加额外的包装元素。
  2. 更灵活的组件结构:可以创建更自然、更符合语义的组件结构。
  3. 更好的 CSS 布局:避免了额外的包装元素对 CSS 布局的影响。
  4. 更简洁的模板:模板代码更加简洁,可读性更好。

注意事项

  1. 属性传递:当组件有多个根节点时,属性不会自动传递到子节点,需要显式指定。
html
<template>
  <h1 :class="titleClass">Hello</h1>
  <p>World</p>
</template>

<script>
export default {
  props: ['titleClass']
};
</script>
  1. 事件处理:当组件有多个根节点时,事件不会自动绑定到子节点,需要显式指定。
html
<template>
  <button @click="handleClick">Click me</button>
  <p>World</p>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('Button clicked');
    }
  }
};
</script>

应用场景

  1. 列表项组件:创建包含多个元素的列表项组件。
  2. 表单控件组件:创建包含标签、输入框和错误信息的表单控件组件。
  3. 布局组件:创建包含多个布局元素的布局组件。
  4. 条件渲染:根据条件渲染不同的根节点。

总结

Fragment 是 Vue 3 中一个非常实用的新特性,它允许组件拥有多个根节点,减少了不必要的 DOM 元素,使组件结构更加灵活和自然。在实际开发中,Fragment 可以帮助我们创建更简洁、更符合语义的组件。